package pathutil

import (
	"errors"
	"golang.org/x/sys/windows/registry"
	"math"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

// 注册表app path路径
const reg_apppath = `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths`

// 默认扩展名
var default_file_exts []string = []string{"", ".exe", ".bat", ".cmd", ".msi", ".msc", ".com"}

// 环境变量格式的正则：%xxx%\aa\bb.exe
var reg_env *regexp.Regexp = regexp.MustCompile("[%][^%]+?[%]")

// 通用错误对象
var general_err = errors.New("still not exist")
var blank_err = errors.New("path is blank")

// 路径解析结果格式
type ParsePathResult struct {
	Succ      bool
	File      bool
	Dir       bool
	OriginStr string
	ValidPath string
	WorkDir   string
}

func init() {
	newItems := filepath.SplitList(os.Getenv("PATHEXT"))
	for _, item := range newItems {
		contains := false
		for _, ext := range default_file_exts {
			if strings.ToLower(item) == strings.ToLower(ext) {
				contains = true
			}
		}
		if !contains {
			default_file_exts = append(default_file_exts, strings.ToLower(item))
		}
	}
}

// ParseValidPath 尝试解析指定字符串为有效路径
// str 指定字符串
// contextDir 当前目录，可为空
func ParseValidPath(str string, ctxDir string) *ParsePathResult {
	ctxDir = strings.TrimSpace(ctxDir)
	str = strings.TrimSpace(str)

	// 上下文路径解析，如果是文件则取所在目录
	// 只按绝对路径判断，不考虑环境变量、注册表应用路径、path变量等情况
	var ctxDirs []string = []string{""}
	ctxDir = trimLeftItems(ctxDir, []string{"file:///", "file://", "./", ".\\"})
	invalidCtxDir := "" == ctxDir || "." == ctxDir || "./" == ctxDir || ".\\" == ctxDir || "/" == ctxDir || "\\" == ctxDir
	hasValidCtxDir := false
	validCtxDir := ""
	if !invalidCtxDir {
		ctxDirSt, e := os.Stat(ctxDir)
		if nil != e {
			ctxDirs = []string{""}
		} else {
			tmp, e := filepath.Abs(ctxDir)
			if nil != e {
				ctxDirs = []string{""}
			} else if !strings.Contains(ctxDir, "/") && !strings.Contains(ctxDir, "\\") {
				ctxDirs = []string{""}
			} else {
				tmp = filepath.Clean(tmp)
				ctxDirs = []string{"", tmp}
				if !ctxDirSt.IsDir() {
					ctxDirs[1] = filepath.Dir(tmp)
				}
				hasValidCtxDir = true
				validCtxDir = ctxDirs[1]
			}
		}
	}

	// 格式简单处理
	// 如果文件路径是不含有效字符，但上下文路径有效，则以上下文路径返回结果
	originStr := str
	ctxDir = strings.TrimSpace(ctxDir)
	str = trimLeftItems(strings.TrimSpace(str), []string{"file:///", "file://", "./", ".\\"})
	str = strings.TrimSpace(strings.ReplaceAll(str, "/", "\\"))
	invalidPath := "." == str || "" == str || "\\" == str || "\\\\" == str
	if invalidPath && hasValidCtxDir {
		return &ParsePathResult{
			Succ:      true,
			File:      false,
			Dir:       true,
			OriginStr: originStr,
			ValidPath: validCtxDir,
			WorkDir:   filepath.Dir(validCtxDir),
		}
	}
	if invalidPath {
		return fillParseValidPathResult(nil, blank_err, str, "", originStr)
	}

	envDict := map[string]string{}

	// 字符串与[上下文路径]、[扩展名]、[环境变量替换]组合后判断存在性
	allPaths, _ := multiEnvEscape(multiExt(multiCtxDir([]string{str}, ctxDirs), default_file_exts), nil, envDict)
	result, err := checkAtLeastOnePathValid(allPaths, nil, originStr)
	if nil == err {
		return result
	}

	// 字符串与[扩展名]组合后匹配注册表app path，匹配结果与[环境变量替换]组合后判断存在性
	paths, dirs := findAppPathMatchItems(multiExt([]string{str}, default_file_exts))
	if 0 < len(paths) {
		paths, dirs = multiEnvEscape(paths, dirs, envDict)
		result, err = checkAtLeastOnePathValid(paths, dirs, originStr)
		if nil == err {
			return result
		}
	}

	// 以path环境变量中路径为[上下文路径]，再与[扩展名]、[环境变量替换]组合后判断
	pathEnvItems := filepath.SplitList(os.Getenv("path"))
	allPaths, _ = multiEnvEscape(multiExt(multiCtxDir([]string{str}, pathEnvItems), default_file_exts), nil, envDict)
	result, err = checkAtLeastOnePathValid(allPaths, nil, originStr)
	if nil == err {
		return result
	}
	return fillParseValidPathResult(nil, general_err, str, "", originStr)
}

// 替换字符串中的环境变量表达式
// %xxx%\yy.exe -> d:\mm\nn\yy.exe
func replEnvParts(str string, envDict map[string]string) string {
	allMatchEnvItem := reg_env.FindAllString(str, -1)
	if nil != allMatchEnvItem && 0 < len(allMatchEnvItem) {
		for _, envItem := range allMatchEnvItem {
			envKey := strings.TrimSuffix(strings.TrimPrefix(envItem, "%"), "%")
			envVal, ok := envDict[envKey]
			if ok {
				str = strings.Replace(str, envItem, envVal, 1)
			} else {
				envVal = os.Getenv(envKey)
				if 0 < len(envVal) {
					envDict[envKey] = envVal
					str = strings.Replace(str, envItem, envVal, 1)
				}
			}
		}
	}
	return str
}

func multiEnvEscape(strs, dirs []string, envDict map[string]string) ([]string, []string) {
	size := len(strs)
	if nil == dirs || len(dirs) < size {
		dirs = make([]string, size)
	}

	for i := 0; i < size; i++ {
		escapedStr := replEnvParts(strs[i], envDict)
		escapedDir := replEnvParts(dirs[i], envDict)
		if escapedStr != strs[i] || escapedDir != dirs[i] {
			strs = append(strs, escapedStr)
			dirs = append(dirs, escapedDir)
		}
	}
	return strs, dirs
}

func multiCtxDir(strs, ctxDirs []string) []string {
	if nil == ctxDirs || 0 == len(ctxDirs) {
		return strs
	}
	result := make([]string, len(strs)*len(ctxDirs))
	ind := 0
	for _, ctxDir := range ctxDirs {
		for _, str := range strs {
			if "" == ctxDir {
				result[ind] = str
			} else {
				result[ind] = filepath.Join(ctxDir, str)
			}
			ind++
		}
	}
	return result
}

func multiExt(strs, exts []string) []string {
	if nil == exts || 0 == len(exts) {
		return strs
	}
	result := make([]string, len(strs)*len(exts))
	ind := 0
	for _, ext := range exts {
		for _, str := range strs {
			result[ind] = str + ext
			ind++
		}
	}
	return result
}

func getAppPathByName(name string) ([]string, error) {
	key, err := registry.OpenKey(registry.LOCAL_MACHINE, reg_apppath+`\`+name, registry.QUERY_VALUE)
	if err != nil {
		return []string{}, err
	}
	defer key.Close()
	val, _, err := key.GetStringValue("")
	if nil != err {
		return []string{}, err
	}
	path, _, err := key.GetStringValue("Path")
	if nil != err {
		path = ""
	}
	return []string{val, path}, nil
}

func getAllRegeditAppPathNames() []string {
	appPathKey, err := registry.OpenKey(registry.LOCAL_MACHINE, reg_apppath, registry.QUERY_VALUE|registry.ENUMERATE_SUB_KEYS)
	if err != nil {
		return []string{}
	}
	defer appPathKey.Close()
	names, _ := appPathKey.ReadSubKeyNames(math.MaxInt)
	if nil == names {
		names = []string{}
	}
	return names
}

// 匹配注册表app path中的路径，但不判断路径有效性
func findAppPathMatchItems(keys []string) ([]string, []string) {
	pathsResult := []string{}
	dirsResult := []string{}
	appPathNames := getAllRegeditAppPathNames()
	for _, appPathName := range appPathNames {
		for _, key := range keys {
			if strings.ToLower(key) == strings.ToLower(appPathName) {
				paths, err := getAppPathByName(appPathName)
				if nil == err {
					pathsResult = append(pathsResult, trimQuote(paths[0]))
					dirsResult = append(dirsResult, trimQuote(paths[1]))
				}
			}
		}
	}
	return pathsResult, dirsResult
}

func trimQuote(str string) string {
	wrapper := `"`
	str = strings.TrimSpace(str)
	if strings.HasPrefix(str, wrapper) {
		str = strings.TrimPrefix(str, wrapper)
	}
	if strings.HasSuffix(str, "\"") {
		str = strings.TrimSuffix(str, wrapper)
	}
	return str
}

// 检查是否至少有一个路径有效
func checkAtLeastOnePathValid(paths, dirs []string, originStr string) (*ParsePathResult, error) {
	if nil == dirs || len(dirs) < len(paths) {
		dirs = make([]string, len(paths))
	}

	for ind, path := range paths {
		stat, err := os.Stat(path)
		_, err2 := os.Stat(dirs[ind])
		pathIsFullpath := strings.Contains(path, "/") || strings.Contains(path, "\\")
		dirIsFullpath := strings.Contains(dirs[ind], "/") || strings.Contains(dirs[ind], "\\")

		if nil == err && pathIsFullpath {
			dir := dirs[ind]
			if nil != err2 || !dirIsFullpath {
				dir = filepath.Dir(path)
			}
			return fillParseValidPathResult(stat, err, path, dir, originStr), nil
		}
	}
	return nil, general_err
}

func fillParseValidPathResult(stat os.FileInfo, err error, path, dir, originStr string) *ParsePathResult {
	if nil != err {
		return &ParsePathResult{
			Succ:      false,
			File:      false,
			Dir:       false,
			OriginStr: originStr,
			ValidPath: "",
			WorkDir:   "",
		}
	}

	if "" == dir {
		dir = filepath.Dir(path)
	}
	return &ParsePathResult{
		Succ:      true,
		File:      !stat.IsDir(),
		Dir:       stat.IsDir(),
		OriginStr: originStr,
		ValidPath: filepath.Clean(path),
		WorkDir:   filepath.Clean(dir),
	}
}

func trimLeftItems(str string, prefixes []string) string {
	for _, prefix := range prefixes {
		str = strings.TrimSpace(strings.TrimPrefix(str, prefix))
	}
	return str
}
